package com.minelx.datamicroscope.querylanguage.data;

import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.sql.ast.statement.*;
import com.alibaba.druid.sql.dialect.mysql.parser.MySqlStatementParser;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.minelx.datamicroscope.querylanguage.data.render.Projection;
import com.minelx.datamicroscope.querylanguage.data.sql.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static com.minelx.datamicroscope.querylanguage.data.CollectingField.aliasOf;
import static java.util.stream.Collectors.toList;

public class QueryBuilder {

	private final List<JSONObject> firstCollectionConditions;

	private final CollectingMapping collectingMapping;

	private final Projection projection;

	private final Map<CollectingKey, String> joiningMethods;

	private QueryBuilder(List<JSONObject> firstCollectionConditions) {
		this.firstCollectionConditions = firstCollectionConditions;

		collectingMapping = new CollectingMapping();
		projection = new Projection();
		joiningMethods = new HashMap<>();
	}

	public void setJoining(CollectingKey collectingKey, String joiningMethod) {
		joiningMethods.put(collectingKey, joiningMethod);
	}

	public void straightCollect(CollectingKey collectingKey, CollectingField collectingField) {
		retains(collectingField.getAlias());
		collect(collectingKey, collectingField);
	}

	public void collect(CollectingKey collectingKey, CollectingField collectingField) {
		if (collectingField.getMethodName() != null) { // aggregation
			projection.addAggregationField(collectingField);
		}
		collectingMapping.put(collectingKey, collectingField);
	}

	public void groupBy(List<String> groupByResourceNames) {
		projection.groupBy(groupByResourceNames);
	}

	private void retains(String retained) {
		projection.retains(retained);
	}

	public JSONObject render() {
		JSONObject result = new JSONObject();
		result.put("projection", projection.renderProjection());
		result.put("collect", renderCollect());
		// put how mappings if needed
		if (!joiningMethods.isEmpty()) {
			result.put("how", renderHow());
		}
		return result;
	}

	private JSONObject renderHow() {
		JSONObject result = new JSONObject();
		List<CollectingGroup> groups = collectingMapping.groups();
		for (Map.Entry<CollectingKey, String> entry : joiningMethods.entrySet()) {
			String resourceName = groups.stream()
					.filter(collectingGroup -> collectingGroup.getCollectingKey().equals(entry.getKey()))
					.map(CollectingGroup::getResourceName)
					.findFirst().orElseThrow(RuntimeException::new);
			result.put(resourceName, entry.getValue());
		}
		return result;
	}

	private JSONArray renderCollect() {
		JSONArray result = new JSONArray();

		List<CollectingGroup> groups = collectingMapping.groups();

		// add first collection
		CollectingGroup firstGroup = groups.get(0);
		result.add(renderFirstCollection(firstGroup, firstCollectionConditions));

		// straightly render and push
		List<CollectingGroup> lastGroups = groups.subList(1, groups.size());
		lastGroups.stream()
				.map(CollectingGroup::render)
				.forEach(result::add);
		return result;
	}

	private JSONObject renderFirstCollection(CollectingGroup collectingGroup, List<JSONObject> contentAndFieldConditions) {
		JSONObject result = collectingGroup.render();
		result.remove("on");
		result.put("conditions", contentAndFieldConditions);
		return result;
	}

	public static JSONObject sql(String sql) {
		SQLSelectQueryBlock firstQueryBlock = firstQueryBlockOf(sql);

		List<ConditionProvider> majorTableConditions = getMajorTableConditions(firstQueryBlock);
		// create content for QueryBuilder
		Conditions conditions = Conditions.create(majorTableConditions);

		// create QueryBuilder
		QueryBuilder result = QueryBuilder.create(conditions.firstCollectionConditions());

		ColumnNames columnNames = createColumnNames(firstQueryBlock);

		// parse aliased tables
		AliasedTables aliasedTables = AliasedTablesBuilder.createAliasedTables(firstQueryBlock, columnNames);

		aliasedTables.getHowMapping().forEach(result::setJoining);

		// create all select items
		List<SelectItem> selectItems = createSelectItems(firstQueryBlock, aliasedTables);

		// push items to QueryBuilder
		selectItems.forEach(selectItem ->
				result.collect(selectItem.getCollectingKey(), selectItem.getCollectingField()));

		// group by if needed
		SQLSelectGroupByClause groupBy = firstQueryBlock.getGroupBy();
		if (groupBy != null) {
			result.groupBy(groupBy.getItems().stream()
					.map(String::valueOf)
					// FIXME 2022/3/30 wait for fixing.     should set column name if not contains
					.map(columnNames::obtainOnly)
					.collect(toList()));
		}

		// drop all temp fields, retain all declared fields
		toAliases(firstQueryBlock.getSelectList()).forEach(result::retains);
		return result.render();
	}

	private static List<SelectItem> createSelectItems(SQLSelectQueryBlock firstQueryBlock,
													  AliasedTables aliasedTables) {
		SQLTableSource fromTable = firstQueryBlock.getFrom();
		if (fromTable instanceof SQLExprTableSource) {
			return SelectItemsFactory.single(firstQueryBlock);
		} else if (fromTable instanceof SQLJoinTableSource) {
			return SelectItemsFactory.joined(firstQueryBlock, aliasedTables);
		} else {
			throw new UnsupportedOperationException("FromTableExpr.createFromTableExpr() encountered an unexpected type of SQLTableSource: " + fromTable.getClass());
		}
	}

	private static ColumnNames createColumnNames(SQLSelectQueryBlock firstQueryBlock) {
		// preset column names of content
		ColumnNames result = ColumnNames.startsAt(1);
		// setting up to column names
		firstQueryBlock.getSelectList()
				.forEach(sqlSelectItem -> result.selectItemSet(
						sqlSelectItem.getExpr().toString(),
						aliasOf(sqlSelectItem)));
		return result;
	}

	static List<ConditionProvider> getMajorTableConditions(SQLSelectQueryBlock firstQueryBlock) {
		SQLTableSource fromTableSource = firstQueryBlock.getFrom();
		if (fromTableSource instanceof SQLExprTableSource) {
			return ConditionProvider.list(firstQueryBlock.getWhere());
		} else if (fromTableSource instanceof SQLJoinTableSource) {
			String leftTableAlias = majorTableAlias((SQLJoinTableSource) fromTableSource);
			return ConditionProvider.list(firstQueryBlock.getWhere()).stream()
					.filter(conditionProvider -> conditionProvider.left().tableAlias().equals(leftTableAlias))
					.collect(toList());
		} else {
			throw new UnsupportedOperationException("QueryBuilder.processConditionProviders is not supported.");
		}
	}

	private static String majorTableAlias(SQLJoinTableSource fromTableSource) {
		SQLTableSource left = fromTableSource.getLeft();
		if (left instanceof SQLJoinTableSource) {
			return majorTableAlias((SQLJoinTableSource) left);
		} else if (left instanceof SQLExprTableSource){
			return left.getAlias();
		} else {
			throw new UnsupportedOperationException("unexpected left table class: " + left.getClass() + ", left: " + left);
		}
	}

	static SQLSelectQueryBlock firstQueryBlockOf(String sql) {
		SQLStatement statement = new MySqlStatementParser(sql).parseStatement();
		return ((SQLSelectStatement) statement).getSelect().getFirstQueryBlock();
	}

	private static List<String> toAliases(List<SQLSelectItem> selectList) {
		return selectList.stream()
				.map(CollectingField::aliasOf)
				.collect(toList());
	}

	public static String strip(String text, String target) {
		if (text.startsWith(target) && text.endsWith(target)) {
			return text.substring(1, text.length() - 1);
		}
		return text;
	}

	public static QueryBuilder create(List<JSONObject> firstCollectionConditions) {
		return new QueryBuilder(firstCollectionConditions);
	}
}
